<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>面向对象</title>
</head>
<body>
<script>
  // 'use strict'
  /**
     1.1 对象的属性类型

     两种属性： 数据属性和访问器属性

     1. 数据属性
     [[Configurable]]: 表示能否通过delete删除属性从而重新定义属性，能否修改属性的特性或者能否把属性修改为访问器属性。默认为true
     [[Enumerable]]: 表示能否通过for in 循环遍历此属性，默认是true
     [[Writable]]: 表示能否修改此属性的值，默认是true
     [[Value]]: 属性的值，默认是undefined

     如果要修改以上属性时，需要使用Object.defineProperty()方法。 三个参数分别为： 属性所在的对象，属性名，一个描述符对象，
     其中描述符对象的属性必须是configurable, enumerable, writable, value

     Configurable, Writable在严格模式下，如果已经设置成false，再进行修改会抛出错误，非严格模式下，无反应，修改不会成功。
     Configurable还有一个其特殊性，如果先声明了不能删除某个属性，然后立马再声明此属性可以删除，此时在进行删除此属性，会抛出错误。
     其实使用Object.defineProperty()可以为一个对象新增属性。
      如果进行创建新对象时，不指定默认configurable, enumerable和writable特性为false
   */
  /**
  let person = {
    name: '张三',
    age: 12,
    sex: '男',
    play: '篮球'
  }
  Object.defineProperty(person, 'name', {
    value: '李四'
  })
   */
  // configurable (age)是否能删除属性
  // Object.defineProperty(person, 'age', {
  //   configurable: false
  // })
  // Object.defineProperty(person, 'age', {
  //   configurable: true
  // })
  // delete person.age;
  // // Writable play属性的属性值是否能修改
  // Object.defineProperty(person, 'play', {
  //   writable: false
  // })
  // person.play = '乒乓球';
  // /   Enumerable 设置sex属性不能遍历  /
  // Object.defineProperty(person, 'sex', {
  //   enumerable: false
  // })
  // // Value 修改name值
  // Object.defineProperty(person, 'name', {
  //   value: '李四'
  // })
  // for(i in person){
  //   console.log(i+':'+person[i]);
  // }
  // console.log(person);
  /**
    2. 访问器属性
        不包括数据值,包含一对，getter和setter函数(非必需)，在读取访问器属性时，调用getter函数，负责返回有效的值(定义的这个属性的值)。写入时访问器属性时，会调用setter函数
      并传入新值，负责如何处理函数
        [[Configurable]] 表示能否通过delete删除属性，默认true
        [[Enumerable]] 表示能否通过for in循环返回属性，默认true
        [[Get]] 在读取属性时调用函数，默认undefined
        [[Get]] 在获取属性时调用函数，默认undefined
      访问属性不能直接定义，必须使用Object.defineProperty()来定义
    作用：通过改变定义属性的一个值，调用get，set函数，去改变另一个属性的值，并返回这个属性之后的属性值，即常用方法为，设置一个属性的值会导致其他属性发生变化
   * */
  /**
  let book = {
    age: 12,
    name: 'zs',
    ageName: 456
  }

  Object.defineProperty(book, 'ageName', {
    get() {
      return this.age
    },
    set(v) {
      if (v == 'book') {
        this.age = this.name + 1
      }
    }
  })
  book.ageName = 'book';
  console.log(book);
  */
  /**
    1.1.2 定义多个属性
    对于对象定义多个属性，使用Object.defineProperties()方法，这个方法可以一次定义多个属性，两个参数，第一个是要添加或修改的对象名称，第
   二个是该对象要添加或修改的属性
   * */
  /**
  const book = {};
  Object.defineProperties(book, {
    year: {
      configurable: true,
      value: '这是属性year'
    },
    ageName: {
      configurable: true,
      value: '这是属性ageName'
    },
    age: {
      get() {
        return '这是set跟get函数的age属性'
      },
      set(v) {
        if (v == 'book') {
          this.name = v + 1
        }
      }
    }
  })
  book.age = 'book'
  console.log(book);
  */
  /*
    1.1.3 读取属性的特性
    Object.getOwnPropertyDescriptor()方法，可以取得给定属性的描述符，
    两个参数：属性所在的对象和要读取其描述符的属性名称
    返回值：一个对象，如果是访问器属性，这个对象的属性有configurable, enumerable, get和set。如果是数据属性，这个对象的属性有configurable,
    enumerable, writable和value

  * */
  /**
  const book = {
    book: '这是属性book'
  }
  Object.defineProperty(book, 'name', {
    configurable: true,
    get() {
      return '这是属性name'
    },
    set(v) {
      if (v == 'book1') {
        this.book = 23
      }
    }
  })
  book.name = 'book1';
  // console.log(book);
  let descriptor = Object.getOwnPropertyDescriptor(book, 'name');
  let descriptor1 = Object.getOwnPropertyDescriptor(book, 'book');
  console.log(descriptor);
  console.log(descriptor1);
  */
  /*
    1.2 创建对象
    1.2.1 工厂模式
      这种模式抽象了创建具体对象的过程,因为在JS中没有类概念(前文讲过), 用一种函数来封装来创建对象
      这个例子中，creatPerson()能够根据接受的参数创建一个包含所有必要信息的Person对象。可以对他进行无数次的调用，每次调用
      都会返回三个属性和一个方法的对象。虽然工厂模式解决了创建多个相识对象的问题，但却没有解决对象识别的问题(怎么知道一个对象的类型)
  * */
  /**
  function creatPerson(name, age, job) {
    let o = new Object();
    o.name = name;
    o.age = age;
    o.job = job;
    o.sayName = function () {
      console.log(this.name);
    }
    return o
  }
  let person1 = creatPerson('张三', 12, 'Web前端');
  let person2 = creatPerson('李四', 14, 'Java');
  console.log(person1);
  console.log(person2);
  */
  /**
    1.2.2 构造函数模式

   在这个例子中Person()函数取代了上个例子中的createPerson()函数，不同点
    1. 没有显式的创建对象
    2. 直接将属性和方法赋给了this对象
    3. 没有return
   此外还要注意构造函数的函数名要大写，非构造函数的函数名小写，这也是一种区分与其他对象，再者构造函数本事也是函数，只不过他可以用来创建对象

   使用步骤：
      1. 创建一个新的对象
      2. 将构造函数的作用域赋给对象(this指向了这个新对象)
      3. 执行构造函数中的代码(为这个新对象添加属性)
      4. 返回新对象
   person1和person2分别保存着Person的一个不同的实例，这两个对象都有一个constructor(构造函数)属性，该属性指向Person
   person1.constructor == Person // true
   person2.constructor == Person // true
   ----------------------------------------------------------------
   对象的属性constructors最初是用来标识对象类型的，但是，对于检测对象类型，还是instanceof操作符要更可靠些

   创建自定义的构造函数意为着将来可以将它的实例标识为一种特定的类型；而这正是构造函数模式胜过工厂模式的地方
  * */
  /**
  function Person(name, age, job) {
    this.name = name;
    this.age = age;
    this.job = job;
    this.sayName = function () {
      console.log(this.name);
    }
  }
  let person1 = new Person('构造函数张三', 12, 'Web前端');
  let person2 = new Person('构造函数李四', 14, 'Java');
  console.log(person1);
  console.log(person2);
  console.log(person1.constructor == Person);
  console.log(person2.constructor == Person);
  console.log(person2 instanceof Object);
  console.log(person2 instanceof Person);
  */
  /**
    1.2.2.1
   将构造函数当做函数
   构造函数与其他函数唯一区别，就在于调用它们的方式不同。不过构造函数也是函数，不存在定义构造函数的特殊语法，任何函数只要是通过new操作符来调用，那它就可以
   作为构造函数，而任何函数，如果不通过new操作符进行调用，那它就跟普通函数一样，

   在这个例子中，(1)展示了构造函数的典型用法，即使用new操作符去创建一个新对象
      (2)不使用new操作符，结果属性和方法都添加给了window(在全局调用一个函数时，this总指向global，在浏览器上也就是window对象)
      (3)使用call或apply方法，改变this的指向，在某个对象的作用域中调用Person()函数，调用后o将拥有该函数的属性和方法
   * */
  // function Person(name, age, job) {
  //   this.name = name;
  //   this.age = age;
  //   this.job = job;
  //   this.sayName = function () {
  //     console.log(this.name);
  //   }
  // }
  // 1. 当做构造函数调用
  // let person1 = new Person('构造函数张三', 12, 'Web前端');
  // console.log(person1);
  // person1.sayName();
  // 2. 当做普通函数调用
  // Person('普通函数李四', 14, 'Java');  // 添加到了window
  // console.log(window.name);
  // window.sayName();
  // 3. 在另一个对象的作用域调用(call, apply方法)
  // let o = new Object();
  // Person.call(o, '另一个对象调用名称', 16, 'Python');
  // console.log(o);
  // o.sayName();
  /**
    1.2.2.2 构造函数的问题
      虽然构造函数好用，也存在缺点。使用构造函数的主要问题，就是每个方法都要在每个实例上重新创建一遍。
   在前面的例子中，person1和person2都有一个名为sayName的方法，但两个方法的Function实例却是不同的，因为在js中函数是对象，
   因此每定义一个函数，也就是每实例化一个对象。从逻辑角度讲，构造函数也是这样定义的

   将这个方法定义的函数挪到外面，这样一来sayName包含的是一个指针指向同一个函数，因此person1和person2对象共享了全局定义的sayName()函数
   但这样虽然是解决了，如果有多个就会在全局创建多个函数，这些问题可以使用原型模式来解决
   * */
  /**
  function Person(name, age, job) {
    this.name = name;
    this.age = age;
    this.job = job;
    this.sayName = function () {
      console.log(this.name);
    };
    // 等同于 new Function(){}
    this.sayName = sayName;
  }
  function sayName() {
    console.log(this.name);
  }
  let person1 = new Person('构造函数张三', 12, 'Web前端');
  let person2 = new Person('构造函数李四', 14, 'Java');
  console.log(person1.sayName == person2.sayName);
  person1.sayName();
  person2.sayName();
  */
  /**
    1.2.3 原型模式
    我们创建的每个函数都有一个prototype(原型)属性，这个属性是一个指针，指向一个对象，这个对象包含可以由特定类型的所有实例共享的属性和方法
  按字面上的理解，那么prototype就是通过调用构造函数而创建的那个对象实例(person1)的原型对象。使用原型对象的好处是可以让所有对象实例共享它所包含
  的属性和方法，换句话来说不必在构造函数中定义对象实例的信息，而是可以将这些信息直接添加到原型对象中
    person1和person2两个实例访问的都是同一组属性和同一个sayName函数
   * */
  /**
  function Person() {
  }
  Person.prototype.name = '原型创建的名称';
  Person.prototype.age = 23;
  Person.prototype.sayName = function () {
    console.log(this.name);
  }
  let person1 = new Person();
  let person2= new Person();
  person1.sayName();
  person2.sayName();
  console.log(person1);
  console.log(person1.sayName == person2.sayName); // true
  */
  /**
      1.2.3.1 理解原型对象
    无论什么时候，只要创建一个新函数，就会根据一组特定的规则为该函数创建一个prototype属性，这个属性指向函数的原型对象。在默认情况下，所有的原型对象
   都会自动获得一个constructor(构造函数)属性, 这个属性是一个指向prototype属性所在函数的指针。

   hasOwnProperty() 检测一个属性是否存在实例(true)中还是原型(false)中

   * */
  // function Person(name, age, job) { // 构造函数
  //   // this.name = name;
  //   this.age = age;
  //   this.job = job;
  //   this.sayName = function () {
  //     console.log(this.name);
  //   };
  // }
  // Person.prototype.ageName = 'ageName';
  // Person.prototype.name = 'name';
  // let person1 = new Person('构造函数张三', 12, 'Web前端');
  // let person2 = new Person('构造函数李四', 14, 'Java');
  // person1.ageName = 'dd';
  // console.log(person1.hasOwnProperty('ageName'));
  // console.log('name' in person2);
  // console.log(person2.hasOwnProperty('name'));
  /*
  1.2.3.2 原型与in操作符
  in 操作符，不管是在原型中或实例中都返回true，而hasOwnProperty()在实例中返回true，原型中返回false。因此当in返回true而hasOwnPeropery()就能判断某个属性是否在对象中
  * */
  function hasOwnPeroperyIn(object, name) {
    return object.hasOwnProperty(name) && (name in object);
  }
  function Person(name, age, job) { // 构造函数
    this.name = name;
    this.age = age;
    this.job = job;
    this.sayName = function () {
      console.log(this.name);
    };
  }
  Person.prototype.ageName = 'ageName';
  Person.prototype.name = 'name';
  let person1 = new Person('构造函数张三', 12, 'Web前端');
  let person2 = new Person('构造函数李四', 14, 'Java');
  let keys = Object.keys(Person.prototype);
  console.log(keys);
  let keyS = Object.getOwnPropertyNames(Person.prototype);
  console.log(keyS);
</script>

</body>
</html>
